Привет, Хабр! Меня зовут Эрик Басаргин, я iOS-разработчик в Surf.
На одном большом проекте мы столкнулись с низкой скоростью сборки — от трёх минут и более. Обычно в таких случаях студии практикуют модуляризацию проектов, чтобы не работать с огромными монолитами. Мы в Surf решили поэкспериментировать и модуляризовать проект с помощью Swift Package Manager — менеджера зависимостей от Apple.
О результатах разработки приложения расскажем в другой статье, а сейчас ответим на главные вопросы: зачем это всё нужно, почему мы выбрали SPM и как делали первые шаги.

Ответ прост — это нативно и ново. Он не создает overhead в виде xcworkspace, как Cocoapods, к примеру. К тому же SPM — open-source проект, который активно развивается. Apple и сообщество исправляют в нем баги, устраняют уязвимости, обновляют вслед за Swift.
Теоретически сборка ускорится из-за самого факта разделения приложения на модули — framework'и. Это значит, что каждый модуль будет собираться только в том случае, когда в него внесли изменения. Но утверждать точно можно будет только по окончанию эксперимента.
Note: Эффективность модуляризации напрямую зависит от правильного разбиения проекта на модули.
Метод разбиения зависит от выбранной архитектуры, типа приложения, его объема и планов на дальнейшее развитие. Поэтому я расскажу о трёх правилах, которых мы стараемся придерживаться при разбиении.
Разделять обобщённую функциональность. Каждый модуль отвечает за какую-то категорию, к примеру:
Разделять flow'ы приложения. Рассмотрим древовидную структуру, где MainFlow — главное flow приложения. Представим, что у нас новостное приложение.
Выносить reusable компоненты в отдельные модули:
Допустим, у нас есть конкретный flow, и мы хотим добавить в него новые функции. Если компонент будет переиспользоваться или это теоретически возможно в будущем — лучше вынести его в отдельный модуль или аналог CommonUIComponents. В других случаях можно оставить компонент локальным.
Такой подход решает проблему с потерей компонентов. Это происходит в больших проектах, и если компонент не задокументировали, то его поддержка и отладка впоследствии станет убыточной.
Рассмотрим создание тривиального тестового проекта. Я использую Multiplatform App project на SwiftUI. Платформа и интерфейс тут не имеют значения.
Note: Чтобы быстро создать Multiplatform App, нужен XCode 12.2 beta.
Создаём проект и видим следующее:

Теперь создадим первый модуль Common:



Note: Достаточно подключать к таргетам только корневые модули. Они не добавлены как подмодули.
Подключение завершено. Теперь достаточно настроить модуль с public интерфейсом — и вуаля, первый модуль готов.
Добавим пакет AdditionalInfo — как Common, но без добавления к таргетам. Теперь изменим Package.swift у Common пакета.

Добавлять больше ничего не нужно. Можно использовать.
Подключим к нашему тестовому проекту SwiftGen и добавим модуль Palette — он будет отвечать за доступ к палитре цветов, утвержденной дизайнером.

Итоговый внешний вид модуля Palette
Модуль Palette мы с вами настроили. Теперь надо настроить запуск SwiftGen, чтобы палитра генерировалась при старте сборки. Для этого заходим в конфигурацию каждого таргета и создаем новую Build Phase — назовём ее Palette generator. Не забудьте перенести эту Build Phase на максимально высокую позицию.
Теперь прописываем вызов для SwiftGen:
Note:

Пример вызова для SwiftGen
Готово — доступ к цветам можно получить следующим образом:
Перечислю то, с чем мы успели столкнуться.
В этом вопросе предлагаю помечтать и подискутировать в комментариях. Тему нужно хорошо изучить, но выглядит она очень интересно. Кстати, уже есть интересная довольно близкая статья о плюсах и минусах SPM.
SPM дает возможность вызывать swift run, если добавить Package.swift в корень вашего проекта. Что это нам дает? К примеру, можно вызвать fastlane или swiftlint. Пример вызова:
На одном большом проекте мы столкнулись с низкой скоростью сборки — от трёх минут и более. Обычно в таких случаях студии практикуют модуляризацию проектов, чтобы не работать с огромными монолитами. Мы в Surf решили поэкспериментировать и модуляризовать проект с помощью Swift Package Manager — менеджера зависимостей от Apple.
О результатах разработки приложения расскажем в другой статье, а сейчас ответим на главные вопросы: зачем это всё нужно, почему мы выбрали SPM и как делали первые шаги.

Почему именно SPM
Ответ прост — это нативно и ново. Он не создает overhead в виде xcworkspace, как Cocoapods, к примеру. К тому же SPM — open-source проект, который активно развивается. Apple и сообщество исправляют в нем баги, устраняют уязвимости, обновляют вслед за Swift.
Делает ли это сборку проекта быстрее
Теоретически сборка ускорится из-за самого факта разделения приложения на модули — framework'и. Это значит, что каждый модуль будет собираться только в том случае, когда в него внесли изменения. Но утверждать точно можно будет только по окончанию эксперимента.
Note: Эффективность модуляризации напрямую зависит от правильного разбиения проекта на модули.
Как сделать эффективное разбиение
Метод разбиения зависит от выбранной архитектуры, типа приложения, его объема и планов на дальнейшее развитие. Поэтому я расскажу о трёх правилах, которых мы стараемся придерживаться при разбиении.
Разделять обобщённую функциональность. Каждый модуль отвечает за какую-то категорию, к примеру:
- CommonAssets — набор ваших Assets'ов и public интерфейс для доступа к ним. Обычно он генерируется с помощью SwiftGen.
- CommonExtensions — набор расширений, к примеру Foundation, UIKit, дополнительные зависимости.
Разделять flow'ы приложения. Рассмотрим древовидную структуру, где MainFlow — главное flow приложения. Представим, что у нас новостное приложение.
- NewFlow — экраны новостей и обзора конкретной новости.
- FavoritesFlow — экран со списком избранных новостей и экран обзора конкретной новости с дополнительным функционалом.
- SettingsFlow — экраны настроек приложения, аккаунта, категорий и т. д.
Выносить reusable компоненты в отдельные модули:
- CommonUIComponents — модуль, который содержит в себе любые небольшие UI-компоненты. Обычно они влезают в один файл.
- Далее список бесконечен и зависит от количества внутренних кастомных UI компонентов. К примеру, нужно создать модули для генерации экранов результата, кастомной коллекции, билдера кастомных алертов и т. д.
Когда нужно выносить компонент в отдельный модуль
Допустим, у нас есть конкретный flow, и мы хотим добавить в него новые функции. Если компонент будет переиспользоваться или это теоретически возможно в будущем — лучше вынести его в отдельный модуль или аналог CommonUIComponents. В других случаях можно оставить компонент локальным.
Такой подход решает проблему с потерей компонентов. Это происходит в больших проектах, и если компонент не задокументировали, то его поддержка и отладка впоследствии станет убыточной.
Создаём проект с использованием SPM
Рассмотрим создание тривиального тестового проекта. Я использую Multiplatform App project на SwiftUI. Платформа и интерфейс тут не имеют значения.
Note: Чтобы быстро создать Multiplatform App, нужен XCode 12.2 beta.
Создаём проект и видим следующее:

Теперь создадим первый модуль Common:
- добавляем папку Frameworks без создания директории;
- создаём SPM-пакет Common.

- Добавляем поддерживаемые платформы в файл Package.swift. У нас это
platforms: [.iOS(.v14), .macOS(.v10_15)]

- Теперь добавляем наш модуль в каждый таргет. У нас это SPMExampleProject для iOS и SPMExampleProject для macOS.

Note: Достаточно подключать к таргетам только корневые модули. Они не добавлены как подмодули.
Подключение завершено. Теперь достаточно настроить модуль с public интерфейсом — и вуаля, первый модуль готов.
Как подключить зависимость у локального SPM-пакета
Добавим пакет AdditionalInfo — как Common, но без добавления к таргетам. Теперь изменим Package.swift у Common пакета.

Добавлять больше ничего не нужно. Можно использовать.
Пример, приближенный к реальности
Подключим к нашему тестовому проекту SwiftGen и добавим модуль Palette — он будет отвечать за доступ к палитре цветов, утвержденной дизайнером.
- Создаем новый корневой модуль по инструкции выше.
- Добавляем к нему корневые каталоги Scripts и Templates.
- Добавляем в корень модуля файл Palette.xcassets и пропишем какие-либо color set'ы.
- Добавляем пустой файл Palette.swift в Sources/Palette.
- Добавим в папку Templates шаблон palette.stencil.
- Теперь нужно прописать конфигурационный файл для SwiftGen. Для этого добавим файл swiftgen.yml в папку Scripts и пропишем в нем следующее:
xcassets:
inputs:
- ${SRCROOT}/Palette/Sources/Palette/Palette.xcassets
outputs:
- templatePath: ${SRCROOT}/Palette/Templates/palette.stencil
params:
bundle: .module
publicAccess: true
output: ${SRCROOT}/Palette/Sources/Palette/Palette.swift

Итоговый внешний вид модуля Palette
Модуль Palette мы с вами настроили. Теперь надо настроить запуск SwiftGen, чтобы палитра генерировалась при старте сборки. Для этого заходим в конфигурацию каждого таргета и создаем новую Build Phase — назовём ее Palette generator. Не забудьте перенести эту Build Phase на максимально высокую позицию.
Теперь прописываем вызов для SwiftGen:
cd ${SRCROOT}/Palette
/usr/bin/xcrun --sdk macosx swift run -c release swiftgen config run --config ./Scripts/swiftgen.yml
Note:
/usr/bin/xcrun --sdk macosx
— очень важный префикс. Без него при сборке вылетит ошибка: «unable to load standard library for target 'x86_64-apple-macosx10.15». 
Пример вызова для SwiftGen
Готово — доступ к цветам можно получить следующим образом:
Palette.myGreen
(Color type in SwiftUI) и PaletteCore.myGreen
(UIColor/NSColor).Подводные камни
Перечислю то, с чем мы успели столкнуться.
- Всплывают ошибки архитектуры и портят всю логику разбиения на модули.
- SwiftLint & SwiftGen не уживаются вместе при подгрузке их через SPM. Причина в разных версиях yml.
- В крупных проектах не получится сразу избавиться от Cocoapods. А разбивать уже созданный проект с закреплёнными версиями подов — настоящее испытание, потому что SPM только развивается и не везде поддерживается. Но SPM и Cocoapods более-менее работают параллельно: разве что поды могут кидать ошибку «MergeSwiftModule failed with a nonzero exit code». Это происходит довольно редко, а решается очисткой и пересборкой проекта.
- На данный момент SPM не позволяет прописать пути поиска библиотек. Приходится явно указывать их с завязкой на
-L$(BUILD_DIR)
.
SPM — замена Bundler?
В этом вопросе предлагаю помечтать и подискутировать в комментариях. Тему нужно хорошо изучить, но выглядит она очень интересно. Кстати, уже есть интересная довольно близкая статья о плюсах и минусах SPM.
SPM дает возможность вызывать swift run, если добавить Package.swift в корень вашего проекта. Что это нам дает? К примеру, можно вызвать fastlane или swiftlint. Пример вызова:
swift run swiftlint --autocorrect.
Больше полезного про iOS — в нашем телеграм-канале Surf iOS Team. Публикуем кейсы, лучшие практики, новости и вакансии Surf. Присоединяйтесь >>